Skip to content

Java 泛型及其陷阱

泛型的诞生

在 Java 5 泛型出来之前,集合中保存的是通用类型 Object。Java 单继承的结构意味着所有元素都基于 Object 类,所以在集合中可以保存任何类型的数据,易于重用。要使用这样的集合,我们先要往集合添加元素。由于 Java 5 版本前的集合只保存 Object,当我们往集合中添加元素时,元素便向上转型成了 Object,从而丢失自己原有的类型特性。这时我们再从集合中取出该元素时,元素的类型变成了 Object

java
// Java 5 之前
ArrayList arrayList = new ArrayList();
arrayList.add(12345);
arrayList.add("上山打老虎");

ListIterator listIterator = arrayList.listIterator();
while (listIterator.hasNext()) {
    Object o = listIterator.next(); // 里面的元素都变成了 Object
}

那么,该怎么将其转回原先具体的类型呢?这里,我们使用了强制类型转换将其转为更具体的类型,这个过程称为对象的“向下转型”。可是我们不能从“Object”看出其就是“整数”或“字符串”,所以除非能确定元素的具体类型信息,否则“向下转型”就是不安全的。也不能说这样的错误就是完全危险的,因为一旦转化了错误的类型,程序就会运行出错,抛出“运行时异常”(RuntimeException)。

java
Integer i = (Integer) arrayList.get(0);
Integer s = (Integer) arrayList.get(1); // java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

无论如何,我们要寻找一种在取出集合元素时确定其具体类型的方法。另外,每次取出元素都要做额外的“向下转型”对程序和程序员都是一种开销。以某种方式创建集合,以确认保存元素的具体类型,减少集合元素“向下转型”的开销和可能出现的错误难道不好吗?这种解决方案就是:参数化类型机制(Parameterized Type Mechanism)。

参数化类型机制可以使得编译器能够自动识别某个 class 的具体类型并正确地执行。举个例子,对集合的参数化类型机制可以让集合仅接受“字符串”这种类型的元素,并以“字符串”类型取出元素。Java 5 版本支持了参数化类型机制,称之为“泛型”(Generic)。

现在可以按以下方式向 ArrayList 中添加 String(字符串):

java
// Java 5 之后
ArrayList<String> stringArrayList = new ArrayList<>();
stringArrayList.add("一二三四五");
stringArrayList.add("上山打老虎");

ListIterator<String> stringListIterator = stringArrayList.listIterator();
while (stringListIterator.hasNext()) {
    String s = stringListIterator.next(); // 里面的元素还是原来的类型
}

泛型的陷阱

泛型应用在多态中可能会出现一些意想不到的问题。

首先创建父类和子类:

java
class Animal {
    void eat() {
        System.out.println("Animal eating...");
    }
}

class Dog extends Animal {
    @Override
    void eat() {
        System.out.println("Dog eating...");
    }
}

class Cat extends Animal {
    @Override
    void eat() {
        System.out.println("Cat eating...");
    }
}

多态化数组

我们先看一下数组参数是如何多态化运行的。

编写一个方法 takeAnimalWithArray,接受参数为 Animal[],内容是遍历这个数组。

java
public static void takeAnimalWithArray(Animal[] animals) {
    for (Animal animal : animals) {
        animal.eat();
    }
}

分别创建父类数组和子类数组作为参数调用 takeAnimalWithArray

  • 父类数组:

    java
    public static void main(String[] args) {
    
        Animal[] animals = {new Animal(), new Cat(), new Dog()};
        takeAnimalWithArray(animals);
    }

    编译并运行:

    java
    Animal eating...
    Cat eating...
    Dog eating...
  • 子类数组

    java
    public static void main(String[] args) {
    
        Dog[] dogs = {new Dog(), new Dog(), new Dog()};
        takeAnimalWithArray(dogs);
    }

    编译并运行:

    java
    Dog eating...
    Dog eating...
    Dog eating...

可以看到,方法参数为数组时,无论传入的引用是父类数组还是子类数组,编译和运行时一切正常。(真的正常吗?请往下看。)

那么把 Array 数组换成 ArrayList 集合后还会这样吗?

多态化集合

将数组替换为泛型集合。

编写一个方法 takeAnimalWithArrayList,接受参数为 ArrayList<Animal>,内容是遍历这个泛型集合。

java
public static void takeAnimalWithArrayList(ArrayList<Animal> animals) {
    for (Animal animal : animals) {
        animal.eat();
    }
}

分别创建父类泛型集合和子类泛型集合作为参数调用 takeAnimalWithArrayList

  • 父类泛型集合:

    java
    public static void main(String[] args) {
    
        ArrayList<Animal> animals = new ArrayList<>();
        animals.add(new Animal());
        animals.add(new Cat());
        animals.add(new Dog());
    
        takeAnimalWithArrayList(animals);
    }

    编译并运行:

    java
    Animal eating...
    Cat eating...
    Dog eating...
  • 子类泛型集合

    java
    public static void main(String[] args) {
    
        ArrayList<Dog> dogs = new ArrayList<>();
        dogs.add(new Dog());
        dogs.add(new Dog());
        dogs.add(new Dog());
    
        takeAnimalWithArrayList(dogs);
    }

    编译时:

    java
    错误: 不兼容的类型: ArrayList<Dog>无法转换为ArrayList<Animal>
        takeAnimalWithArrayList(dogs);
                                ^

    想不到啊,看起来没有问题,编译时却报错。

    本来用数组没有问题,改成集合为什么就不行了呢?要解决这个问题,我们先假设一下:如果编译可以通过会怎么样?

    假如下面这个方法传入 ArrayList<Dog> 作为参数后程序可以编译通过:

    java
    public static void takeAnimalWithArrayList(ArrayList<Animal> animals) {
        for (Animal animal : animals) {
            animal.eat();
        }
    }

    那么程序应该是可以正常运行的(其实不能运行),就像多态化数组中的子类数组一样。

    但如果方法是这样:

    java
    public static void takeAnimalWithArrayList(ArrayList<Animal> animals) {
        animals.add(new Cat());
    }

    这就有问题了。理论上 ArrayList<Animal> 添加 Cat 是合法的,毕竟 CatAnimal 的子类。

    可是我们传入的其实是一个 ArrayList<Dog> 参数,将 Cat 添加到一个 ArrayList<Dog> 中肯定不合适,这就是为什么编译失败的原因。

    如果把方法参数定义为 ArrayList<Animal>,它就只能传入 ArrayList<Animal>ArrayList<Dog> 还是 ArrayList<Cat> 都不行。

数组类型与集合类型的区别

同样的问题会发生在数组上吗?可以把 Cat 添加到一个 Dog[] 中吗?

java
public static void takeAnimalWithArray(Animal[] animals) {
    animals[0] = new Cat();
}

可以编译,但运行时:

java
Exception in thread "main" java.lang.ArrayStoreException: Cat
    at GenericsTest.takeAnimalWithArray(GenericsTest.java:42)
    at GenericsTest.main(GenericsTest.java:28)

现在知道多态化数组中的子类数组为什么可以运行了。

数组的类型是在运行期间检查的,但集合的类型在编译期间就开始了检查。

怎么才能使用多态化集合参数呢?如何创建接受 Animal 子类的方法?

带有通配符与边界的泛型集合

使用上界通配符 <? extends T>

<? extends T> 声明 ? 必须是 TT 的子类。 在这里我们需要接受 AnimalAnimal 的子类,实际的参数为 ArrayList<? extends Animal>

java
public static void main(String[] args) {

    ArrayList<Dog> dogs = new ArrayList<>();
    dogs.add(new Dog());
    dogs.add(new Dog());
    dogs.add(new Dog());

    takeAnimalWithArrayList(dogs);

    ArrayList<Animal> animals = new ArrayList<>();
        animals.add(new Animal());
        animals.add(new Cat());
        animals.add(new Dog());

    takeAnimalWithArrayList(animals);
}

public static void takeAnimalWithArrayList(ArrayList<? extends Animal> animals) {
    for (Animal animal : animals) {
        animal.eat();
    }
}

编译和运行时一切正常。

相同功能的另一种语法

java
public static <T extends Animal> void takeAnimalWithArrayList(ArrayList<T> animals) {
    for (Animal animal : animals) {
        animal.eat();
    }
}

有什么区别?这要看是否会用到 T

例如,有多个集合参数都继承 Animal,只声明一次更有效率:

java
public static <T extends Animal> void takeAnimalWithArrayList(ArrayList<T> one, ArrayList<T> two) {
}

再比如,方法需要返回 T

java
public static <T extends Animal> T takeAnimalWithArrayList(ArrayList<T> animals) {
}

现在可以愉快的在方法中操作 ArrayList<Animal> 了,再添加一个 Dog 进来:

java
public static void takeAnimalWithArrayList(ArrayList<? extends Animal> animals) {
    for (Animal animal : animals) {
        animal.eat();
    }
    animals.add(new Dog());
}

编译时:

java
错误: 对于add(Dog), 找不到合适的方法
        animals.add(new Dog());
               ^

为什么不能添加 DogCatAnimal 也不行吗?都不行,原因是 ArrayList<? extends Animal> 会将传递进来的类型自动向上转型为 Animal,也就是说编译器只知道集合中的元素是 AnimalAnimal 的子类,具体是什么类型不知道,往集合里面添加 DogCat 还是 Animal 编译器都不能保证能和原有的类型匹配,所以都不能添加。

在方法参数中使用上界通配符时,编译器会阻止任何试图改变引用参数所指集合的行为。也就是说,你可以获取集合元素,但不能新增集合元素(只能取,不能存,null 是个例外)。

可是需要添加 Dog 怎么办?

使用下界通配符 <? super T>

<? super T> 声明 ? 必须是 TT 的父类。 在这里我们需要接受 DogDog 的父类,实际的参数为 ArrayList<? super Dog>

java
public static void main(String[] args) {

    ArrayList<Dog> dogs = new ArrayList<>();
    dogs.add(new Dog());
    dogs.add(new Dog());
    dogs.add(new Dog());

    takeAnimalWithArrayList(dogs);

    ArrayList<Animal> animals = new ArrayList<>();
        animals.add(new Animal());
        animals.add(new Cat());
        animals.add(new Dog());

    takeAnimalWithArrayList(animals);
}

public static void takeAnimalWithArrayList(ArrayList<? super Dog> dogs) {
    dogs.add(new Dog());
}

编译和运行时一切正常。

现在可以愉快的在方法中操作 ArrayList<Dog> 了,再添加一个 Cat 进来:

java
public static void takeAnimalWithArrayList(ArrayList<? super Dog> dogs) {
    dogs.add(new Dog());
    dogs.add(new Cat());
}

编译时:

java
错误: 对于add(Cat), 找不到合适的方法
        dogs.add(new Cat());
            ^

为什么不能添加 CatAnimal 也不行吗?都不行,虽然 ArrayList<? super Dog> 接受 DogDog 的父类,但是会被编译器自动向下转型为 Dog。由于 CatAnimal 不是 Dog 的子类,所以,是不能往集合里面添加 Cat 的。

可是需要添加 Dog 又需要添加 Cat 甚至是 Animal 怎么办?将下界的范围扩大,使用 ArrayList<? super Animal>

java
public static void main(String[] args) {

    ArrayList<Animal> animals = new ArrayList<>();
    animals.add(new Animal());
    animals.add(new Cat());
    animals.add(new Dog());

    takeAnimalWithArrayList(animals);
}

public static void takeAnimalWithArrayList(ArrayList<? super Animal> animals) {
    animals.add(new Dog());
    animals.add(new Cat());
    animals.add(new Animal());
}

编译和运行时一切正常。

现在可以愉快的在方法中操作 ArrayList<Animal> 了,遍历一下:

java
public static void takeAnimalWithArrayList(ArrayList<? super Animal> animals) {
    animals.add(new Dog());
    animals.add(new Cat());
    animals.add(new Animal());
    for (Animal animal : animals) {
        animal.eat();
    }
}

编译时:

java
错误: 不兼容的类型: CAP#1无法转换为Animal
        for (Animal animal : animals) {
                             ^
  其中, CAP#1是新类型变量:
    CAP#1从? super Animal的捕获扩展Object 超 Animal
1 个错误

为什么不能遍历呢?不能,原因是编译器已经将元素的类型都转成了 Object,获取元素的时候不能保证能和原有的类型匹配,所以无法进行遍历。

在方法参数中使用下界通配符时,编译器会阻止任何试图获取引用参数所指集合元素的行为。也就是说,你可以新增集合元素,但不能获取集合元素(只能存,不能取,取出来的都是 Object)。

Released under the MIT License.